using System;
using System.Collections.Generic;
using System.Text;
using MacomberMap.Common.Database_Interface;
using MacomberMap.Common.Database_Interface.CIM;
using System.Threading;
using MacomberMap.Common.Logging;
using MacomberMap.Common.Events;
using MacomberMap.Common.Internals;
using MacomberMap.Common.Components;
using MacomberMap.Common.Database_Interface.CIM.Components;
using System.Drawing;
using MacomberMap.Common.Types;
using MacomberMap.Common.User_Interfaces.Components;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.IO;
using System.Xml;
using System.Windows.Forms;
using Oracle.DataAccess.Client;

namespace MacomberMap.Loader.Merging
{
    /// <summary>
    /// This class provides the routines for synchronizing CIM and Oracle data
    /// </summary>
    public class MM_CIM_Oracle_Synchronization
    {

        #region Variable declarations
        /// <summary>The number of elements needing post-processing</summary>
        public int NumElements = 0;

        /// <summary>The current element being post-processed</summary>
        public int CurElement = 0;

        /// <summary>The Oracle database for synchronization</summary>
        public MM_Database_Oracle OracleDb;

        /// <summary>The CIM database for synchronization</summary>
        public MM_Database_CIM CIMDb;

        /// <summary>The thread in which the synchronization is occurring</summary>
        public Thread ProcessThread;

        /// <summary>The time the merge process was started </summary>
        public DateTime StartTime;

        /// <summary>Whether the synchronziation has completed</summary>
        public bool Completed = false;

        /// <summary>Whether coordinates (lat/lng) should be updated from CIM</summary>
        public static bool CoordinatesFromCIM = false;

        /// <summary>Whether long names should be updated from CIM</summary>
        public static bool LongNamesFromCIM = false;

        /// <summary>Whether the initial load is occuring (and therefore, we don't need to query existing SQL</summary>
        public bool InitialLoad = false;

        /// <summary>The cllection of synchroscopes</summary>
        public Dictionary<String, bool> Synchroscopes = new Dictionary<string, bool>(StringComparer.CurrentCultureIgnoreCase);

        /// <summary>Track the substation coordinates</summary>
        public Dictionary<string, string[]> SubCoordinates = new Dictionary<string, string[]>(StringComparer.CurrentCultureIgnoreCase);
        #endregion

        #region Initialization
        /// <summary>
        /// Initialize the CIM to Oracle Synchronization
        /// </summary>
        /// <param name="OracleDb">The Oracle Database</param>
        /// <param name="CIMDb">The CIM database</param>
        /// <param name="InitialLoad">Whether this load is an initial database load, and therefore no exisiting SQL lookups need to take place</param>
        /// <param name="CoordinateFile">The coordinate file</param>
        public MM_CIM_Oracle_Synchronization(MM_Database_Oracle OracleDb, MM_Database_CIM CIMDb, bool InitialLoad, String CoordinateFile)
        {
            this.InitialLoad = InitialLoad;
            this.OracleDb = OracleDb;
            this.CIMDb = CIMDb;
            string[] splStr;
            if (!String.IsNullOrEmpty(CoordinateFile))
                using (StreamReader sRd = new StreamReader(CoordinateFile))
                {
                    sRd.ReadLine();
                    while (!sRd.EndOfStream)
                    {
                        splStr = sRd.ReadLine().Split(',');
                        SubCoordinates.Add(splStr[0], splStr);
                    }
                }
                
            NumElements = MM_Database_CIM.TEIDs.Count;
            ProcessThread = new Thread(MatchDatabases);
            ProcessThread.Name = "CIM to MM Database Synchronization process"; 
            StartTime = DateTime.Now;
            ProcessThread.Start();
        }

        #endregion 


        #region Synchronization

        /// <summary>
        /// Match the databases against each other 
        /// </summary>
        private void MatchDatabases()
        {            

            //Load the collection of synchroscopes            
            using (StreamReader sRd = new StreamReader(System.Windows.Forms.Application.StartupPath + "\\Synchroscopes.txt", true))
                while (!sRd.EndOfStream)
                {
                    String inLine = sRd.ReadLine();
                    if (Synchroscopes.ContainsKey(inLine))
                        Console.WriteLine("Duplicate synchroscope: " + inLine);
                    else
                        Synchroscopes.Add(inLine, true);
                }

            //Go through all of the zones, and make sure they match up.
            foreach (CIM_Element Zone in MM_Database_CIM.ElementsOfType("cim:SubGeographicalRegion"))
            {
                LocateZone(MM_Repository.TitleCase(Zone.Name), MM_Overall_Display.ElemTypes["WeatherZone"]);
                CurElement++;
            }
            foreach (CIM_Element Zone in MM_Database_CIM.ElementsOfType("etx:SettlementLoadZone"))
            {
                LocateZone(MM_Repository.TitleCase(Zone.Name), MM_Overall_Display.ElemTypes["LoadZone"]);
                CurElement++;
            }
            
            //Pull in the companies, to make sure they're in memory.
            foreach (CIM_Element Company in MM_Database_CIM.ElementsOfType("cim:Company"))
                MatchElement(MM_Overall_Display.FindElementType("Company"), UInt64.Parse(Company.TEID), Company, MM_Repository.TEIDs);


            //Go through all substations, and match them up
            foreach (CIM_Element Substation in MM_Database_CIM.ElementsOfType("cim:Substation"))
            {
                String[] splStr;
                if (SubCoordinates.TryGetValue(Substation.Name, out splStr))
                    MatchElement(MM_Overall_Display.ElemTypes["Substation"], Convert.ToUInt64(Substation.TEID), Substation, MM_Repository.TEIDs, "Longitude", float.Parse(splStr[4]), "Latitude", float.Parse(splStr[5]), "LongName", MM_Repository.TitleCase(splStr[1]));
                else
                    MatchElement(MM_Overall_Display.ElemTypes["Substation"], Convert.ToUInt64(Substation.TEID), Substation, MM_Repository.TEIDs);
                CurElement++;
            }

            //Now, pull together each line segment
            foreach (CIM_Element Line in MM_Database_CIM.ElementsOfType("cim:ACLineSegment"))
                MatchElement(MM_Database_CIM.LocateElementType(Line), UInt64.Parse(Line.TEID), Line, MM_Repository.TEIDs);

            //Do the same for series compensators
            foreach (CIM_Element Line in MM_Database_CIM.ElementsOfType("cim:SeriesCompensator"))
                MatchElement(MM_Database_CIM.LocateElementType(Line), UInt64.Parse(Line.TEID), Line, MM_Repository.TEIDs);

            
                
            //Now, go through everything else, and match them up            
            Dictionary<UInt64, MM_Element> AddedElements = new Dictionary<ulong, MM_Element>();
            //First, process all elements except for electrical buses.
            foreach (KeyValuePair<UInt64, CIM_Element> Elem in new Dictionary<UInt64, CIM_Element>(MM_Database_CIM.TEIDs))
                if (!MM_Repository.TEIDs.ContainsKey(Elem.Key))
                {
                    MM_Element_Type FoundType = LocateElementType(Elem.Value);
                    if (FoundType == null)
                        Console.WriteLine("Don't know what to do with " + Elem.Value.Type);
                    else if (FoundType.Name == "Substation")
                    { }
                    else if (FoundType.Name == "Line")
                    { }
                    else if (FoundType.Name == "Breaker")
                    {
                        bool HasSynchroscope = Synchroscopes.ContainsKey(Elem.Value.Substation.Name + "." + Elem.Value.Name) || Synchroscopes.ContainsKey(Elem.Value.Substation.Name);
                        MatchElement(FoundType, Elem.Key, Elem.Value, AddedElements, "HasSynchroscope", HasSynchroscope);
                    }
                    else if (FoundType.Name == "ElectricalBus")
                    { }
                    else
                        MatchElement(FoundType, Elem.Key, Elem.Value, AddedElements);
                    CurElement++;
                    if (AddedElements.Count == 100)
                        AddedElements.Clear();
                }

            //Now do the same just for electrical buses.                        
            using (StreamWriter sW = new StreamWriter("ElectricalBus.rio", false, Encoding.ASCII))
            {
                sW.WriteLine("hdbrio -f ems -a rtnet netmom");
                foreach (KeyValuePair<UInt64, CIM_Element> Elem in MM_Database_CIM.TEIDs)
                    if (!MM_Repository.TEIDs.ContainsKey(Elem.Key))
                    {
                        MM_Element_Type FoundType = LocateElementType(Elem.Value);

                        if (FoundType != null && FoundType.Name == "ElectricalBus")
                        {
                            sW.WriteLine("find ebs=" + Elem.Value.Name + "; /TEID=" + Elem.Key.ToString() + ";");
                            MatchElement(FoundType, Elem.Key, Elem.Value, AddedElements);
                        }
                    }
            }

            //Now, handle DC Ties
            foreach (CIM_Element DCTie in MM_Database_CIM.ElementsOfType("etx:DCTie"))
            {
                MM_DCTie Tie = MM_Database_CIM.LocateElement(DCTie, AddedElements) as MM_DCTie;                                    
                MatchElement(Tie.ElemType, 0, DCTie, AddedElements);
            }          

            //Now, go through to detect removed CIM elements
            if (!InitialLoad)
            using (OracleCommand oCmd = new OracleCommand("SELECT * FROM MM_ELEMENTS", OracleDb.oConn))
            using (OracleDataReader oRd = oCmd.ExecuteReader())
            {
                while (oRd.Read())
                {
                    UInt64 TEID = Convert.ToUInt64(oRd["TEID"]);
                    MM_Element_Type ElemType = MM_Overall_Display.ElemTypes[Convert.ToInt32(oRd["ElemType"])];
                    bool IsOk = false;
                    CIM_Element FoundElement;
                    if (MM_Database_CIM.TEIDs.TryGetValue(TEID, out FoundElement))
                        if (String.Equals((string)oRd["ElemName"], FoundElement.Name,StringComparison.CurrentCultureIgnoreCase) && MM_Database_CIM.ValidateType(ElemType, FoundElement))
                            IsOk = true;
                    
                    //If we have the element of same name and type in CIM, we would have already flagged the 'TEID change' event, so we don't need to duplicate it.
                    if (!IsOk && oRd["Substation"] is DBNull)
                        FoundElement = MM_Database_CIM.LocateElement("", ElemType.Name, (string)oRd["Name"], "");
                    else if (!IsOk)
                        FoundElement = MM_Database_CIM.LocateElement((string)oRd["Substation"], ElemType.CIMName, (string)oRd["Name"], "");

                    
                }
                oRd.Close();
            }
            
            Completed = true;             
        }
        #endregion


        #region Element synchronization routines        

        /// <summary>
        /// Match an element against its value in the database
        /// </summary>
        /// <param name="ElemType">The type of the element</param>
        /// <param name="TEID"></param>
        /// <param name="BaseElement"></param>
        /// <param name="Parameters"></param>
        /// <param name="AddedElements"></param>
        private void MatchElement(MM_Element_Type ElemType, UInt64 TEID, CIM_Element BaseElement, Dictionary<UInt64, MM_Element> AddedElements, params object[] Parameters)
        {
            String ElemName = BaseElement.Type == "cim:CombinedCyclePlant" ? BaseElement["cim:IdentifiedObject.aliasName"] : BaseElement.Name;


            //Search in the database for the appropriate line
            MM_Element FoundElement = null;
            //First, try and retrieve the item in the database by TEID
            if (!InitialLoad)
                FoundElement = OracleDb.LocateElement(TEID, ElemType);

            //If we found an element of the wrong type, we have to recognize the TEID change
            if (FoundElement != null && FoundElement.ElemType != ElemType)
                FoundElement = null;

            //If we can't locate the item by TEID, search by substation name and element name
            if (!InitialLoad && FoundElement == null)
                FoundElement = OracleDb.LocateElement(BaseElement.Substation.Name, BaseElement.Name, ElemType);

            //If we have an element found, check for TEID match.
            if (!InitialLoad && TEID != 0 && FoundElement != null && TEID != FoundElement.TEID)
                MM_Log.LogEvent(new MM_Event_ValueChange(FoundElement, FoundElement.TEID, TEID, "TEID"));


            //Create our new element and assign it the special parameters
            MM_Element NewElement = MM_Database_CIM.LocateElement(BaseElement, AddedElements);
            for (int a = 0; a < Parameters.Length; a += 2)
            {
                MemberInfo OutTarget = NewElement.GetType().GetMember((string)Parameters[a])[0];
                if (OutTarget is FieldInfo)
                    (OutTarget as FieldInfo).SetValue(NewElement, Parameters[a + 1]);
                else if (OutTarget is PropertyInfo)
                    (OutTarget as PropertyInfo).SetValue(NewElement, Parameters[a + 1], null);
            }


            //Now compare all values, or flag the new element
            if (FoundElement == null)
                MM_Log.LogEvent(new MM_Event_NewElement(NewElement));
            else
                foreach (MemberInfo mI in MM_Serializable.GetMembers(FoundElement))                    
                    foreach (object obj in mI.GetCustomAttributes(true))
                        if (obj is MM_Serializable_Target && (obj as MM_Serializable_Target).IsTypeSpecific)
                        {
                            object curValue = null, newValue = null;
                            if (mI is FieldInfo)
                            {
                                curValue = (mI as FieldInfo).GetValue(FoundElement);
                                newValue = (mI as FieldInfo).GetValue(NewElement);
                            }
                            else if (mI is PropertyInfo)
                            {
                                curValue = (mI as PropertyInfo).GetValue(FoundElement, null);
                                newValue = (mI as PropertyInfo).GetValue(NewElement, null);
                            }
                            if (!object.Equals(curValue, newValue))
                                MM_Log.LogEvent(new MM_Event_ValueChange(FoundElement, curValue, newValue, mI.Name));
                        }
        }
            
        

                                        
                

      



        /// <summary>
        /// Locate the element type corresponding to the element
        /// </summary>
        /// <param name="Elem">The voltage level to check</param>
        /// <returns></returns>
        private MM_Element_Type LocateElementType(CIM_Element Elem)
        {
            if (Elem == null)
                return null;

            foreach (MM_Element_Type ElemType in MM_Overall_Display.ElemTypes.Values)
                if (MM_Database_CIM.ValidateType(ElemType, Elem))
                    return ElemType;
            return null;
        }

      
        /// <summary>
        /// Locate or create a zone with the specified name
        /// </summary>
        /// <param name="ZoneName">The name to search for</param>
        /// <param name="ZoneType">The type of zone being searched for</param>
        /// <returns></returns>
        private MM_Zone LocateZone(string ZoneName, MM_Element_Type ZoneType)
        {
            MM_Zone OutZone;
            if (String.IsNullOrEmpty(ZoneName))
                return null;
            if (!MM_Repository.Zones.TryGetValue(ZoneName, out OutZone))
            {
                MM_Zone NewZone = new MM_Zone(ZoneName, 0, ZoneType);
                MM_Repository.Zones.Add(ZoneName, NewZone);
                MM_Log.LogEvent(new MM_Event_NewElement(NewZone));
            }
            else if (OutZone.ElemType != ZoneType)
                MM_Log.LogEvent(new MM_Event_ValueChange(OutZone, OutZone.ElemType, ZoneType, "ElemType"));
            return OutZone;
        }
        #endregion
    }
}
